﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace Concat
{
    class Interpreter
    {
        public int StackOverflowLevel { get; set; }

        Dictionary<string, Word> vocab;
        Dictionary<string, ConcatType> types;
        Dictionary<string, MultiMethod> multimethods;
        Stack<object> data;
        CallStack calls;

        public Interpreter(int stackOverflowLevel)
        {
            vocab = new Dictionary<string, Word>();
            types = new Dictionary<string, ConcatType>();
            multimethods = new Dictionary<string, MultiMethod>();
            data = new Stack<object>();
            calls = new CallStack();

            StackOverflowLevel = stackOverflowLevel;
        }

        public void define(DefTerm def)
        {
            vocab[def.name] = new Word(def.bodyLoc, def.formals, def.def, new HashSet<string>());
        }
        public void define(string name, BuiltinWord word)
        {
            vocab[name] = word;
        }
        public void define(string name, string[] formals, BuiltinFunc func)
        {
            vocab[name] = new BuiltinWord(formals, func);
        }
        public void define(Tuple<string, BuiltinWord> builtin)
        {
            define(builtin.Item1, builtin.Item2);
        }
        public void defineAll(object host)
        {
            foreach (var word in BuiltinCreator.getDefs(this, host))
                define(word);
        }

        public void defineType(ConcatType type)
        {
            types[type.name] = type;
            if (type is StructType)
                define("<" + type.name + ">", new string[0], args => new object[1] { ((StructType)type).new_obj() });
            define("is-" + type.name + "?", new string[1] { "obj" }, args =>
                new object[1] { args[0] is StructObject
                              ? type.covers(((StructObject)args[0]).type)
                              : false });
            define("|" + type.name + "|", new string[1] { "obj" }, args =>
            {
                if (args[0] is StructObject)
                    ((StructObject)args[0]).effectiveType = type;
                return args;
            });
        }
        public void defineStructType(string name, string[] slots)
        {
            defineType(new StructType(name, slots, new StructType[0]));
        }
        public void extendStructType(string name, string parent, string[] slots)
        {
            StructType child = new StructType(name, slots, new StructType[0]);
            ((StructType)types[parent]).addChild(child);
            defineType(child);
        }
        public void defineClassType(string name, DefMethodTerm[] methodTerms)
        {
            ClassType temp = new ClassType(name, null);
            types[name] = temp;
            temp.protocol = methodTerms.map(term => getMethod(term)).ToArray();
            defineType(temp);
        }
        public void defineExtendClassType(string name, string parent, DefMethodTerm[] methodTerms)
        {
            ClassType child = new ClassType(name, ((ClassType)types[parent]), methodTerms.map(term => getMethod(term)).ToArray());
            defineType(child);
        }
        public void includeClassMember(string className, string memberName)
        {
            ClassType _class = (ClassType)types[className];
            StructType member = (StructType)types[memberName];
            if (_class.followsProtocol(member, multimethods.Values.ToArray()))
                _class.addMember(member);
            else
                throw new Exception("Struct '" + memberName + "' does not implement all of class protocol '" + className + "'.");
        }
        private Method getMethod(DefMethodTerm dmt)
        {
            return new Method(
                dmt.types.map(type => types[type]).ToArray(),
                dmt.formals, dmt.paramNames, dmt.name, dmt.bodyLoc, dmt.def);
        }
        public void defMethod(DefMethodTerm dmt)
        {
            MultiMethod multi;
            if (multimethods.ContainsKey(dmt.name))
                multi = multimethods[dmt.name];
            else
            {
                multi = new MultiMethod(dmt.name);
                multimethods[dmt.name] = multi;
            }
            multi.addMethod(getMethod(dmt));
        }

        [System.Diagnostics.DebuggerNonUserCode]
        public void push(object obj)
        {
            data.Push(obj);
        }
        [System.Diagnostics.DebuggerNonUserCode]
        public object pop()
        {
            return data.Pop();
        }
        [System.Diagnostics.DebuggerNonUserCode]
        public T pop<T>()
        {
            object obj = data.Pop();
            if (obj is T)
                return (T)obj;
            else
                throw new InvalidCastException("Popped value was of type '" + obj.GetType().ToString() + "'.");
        }
        [System.Diagnostics.DebuggerNonUserCode]
        public int stackSize()
        {
            return data.Count;
        }

        [System.Diagnostics.DebuggerNonUserCode]
        public void clearCallStack()
        {
            //callDepthStack = new Stack<int>();
            calls = new CallStack();
        }
        [System.Diagnostics.DebuggerNonUserCode]
        public void clearDataStack()
        {
            data = new Stack<object>();
        }

        [System.Diagnostics.DebuggerNonUserCode]
        public object[] ensureStack(Type[] types, Action action)
        {
            int count = data.Count;
            action();
            if (data.Count != count + types.Length)
                throw new InvalidStackEffectException(types, calls);
            object[] result = new object[types.Length];
            for (int i = 0; i < result.Length; i++)
            {
                result[i] = data.Pop();
                if (result[i].GetType() != types[i])
                    throw new InvalidStackEffectException(types, calls);
            }
            return result;
        }
        [System.Diagnostics.DebuggerNonUserCode]
        public void ensureStack(int consumption, Action action)
        {
            int count = data.Count;
            action();
            if (data.Count != count - consumption)
                throw new InvalidStackEffectException(consumption, calls);
        }

        [System.Diagnostics.DebuggerNonUserCode]
        public void execTopLevel(Term term)
        {
            calls.setProgLoc(term.loc);
            exec(term);
        }
        [System.Diagnostics.DebuggerNonUserCode]
        public void execProg(params Term[] terms)
        {
            terms.each(execTopLevel);
        }
        //[System.Diagnostics.DebuggerNonUserCode]
        public void execFile(string file)
        {
            using (StreamReader reader = new StreamReader(File.OpenRead(file)))
            {
                Scanner scanner = new Scanner(file, reader);
                Parser parser = new Parser(scanner);
                Term[] terms = parser.parseAll();
                foreach (Term term in terms) 
                    execTopLevel(term);
                //terms.each(execTopLevel);
            }
        }

        [System.Diagnostics.DebuggerNonUserCode]
        public CallFrame exec(Quote quote, bool canRunImmediately = false, bool frameStopsExecution = true,
            Quote handler = null)
        {
            quote.setFormals(data);
            CallFrame frame = call("*quote", quote.terms, quote.closure, 
                quote.ignoredDynamicWords, handler, canRunImmediately);
            frame.stopExecution = frameStopsExecution;
            return frame;
        }

        [System.Diagnostics.DebuggerNonUserCode]
        public CallStack getCallStack()
        {
            return calls;
        }

        public void setDynamicWord(string name, Word word)
        {
            calls.setDynamicWord(name, word);
        }

        bool newCall = false;
        [System.Diagnostics.DebuggerNonUserCode]
        public void exec(Term term)
        {
            if (term is LiteralTerm) handleLiteral((LiteralTerm)term);
            else if (term is IdentTerm) handleIdent((IdentTerm)term);
            else if (term is DefTerm) define((DefTerm)term);
            else if (term is StructTerm) defineStructType(((StructTerm)term).name, ((StructTerm)term).slots);
            else if (term is ExtendStructTerm)
                extendStructType(((ExtendStructTerm)term).name, ((ExtendStructTerm)term).parent,
                    ((ExtendStructTerm)term).slots);
            else if (term is ClassTerm) defineClassType(((ClassTerm)term).name, ((ClassTerm)term).protocol);
            else if (term is ExtendClassTerm) defineExtendClassType(((ExtendClassTerm)term).name, ((ExtendClassTerm)term).parent,
                ((ExtendClassTerm)term).protocol);
            else if (term is ClassMemberTerm)
            {
                string className = ((ClassMemberTerm)term).className;
                foreach (string memberName in ((ClassMemberTerm)term).memberNames)
                    includeClassMember(className, memberName);
            }
            else if (term is DefMethodTerm) defMethod((DefMethodTerm)term);
            else throw new Exception("Forgot a case in Interpreter.exec(Term)...");
        }
        [System.Diagnostics.DebuggerNonUserCode]
        void handleLiteral(LiteralTerm term)
        {
            if (term is IntTerm)
                data.Push(((IntTerm)term).value);
            else if (term is NumTerm)
                data.Push(((NumTerm)term).value);
            else if (term is StrTerm)
                data.Push(((StrTerm)term).data);
            else if (term is SymTerm)
                data.Push(new Symbol(((SymTerm)term).symbol));
            else if (term is QuoteTerm)
                data.Push(new Quote(term.loc, calls, ((QuoteTerm)term).formals, ((QuoteTerm)term).terms));
            else if (term is ObjTerm)
                data.Push(((ObjTerm)term).obj);
            else
                throw new Exception("Forgot a case type in Interpreter.handleLiteral(LiteralTerm)...");
        }
        //[System.Diagnostics.DebuggerNonUserCode]
        void handleIdent(IdentTerm term)
        {
            string ident = ((IdentTerm)term).ident;

            if (calls.hasarg(ident))
                data.Push(calls.getarg(ident));
            else if (vocab.ContainsKey(ident) || calls.hasDynamicWord(ident))
            {
                Word word = calls.hasDynamicWord(ident)
                          ? calls.getDynamicWord(ident)
                          : vocab[ident];

                if (data.Count < word.formals.Length)
                    throw new StackUnderflowException((IdentTerm)term, calls);
                object[] args = new object[word.formals.Length];
                for (int i = 0; i < args.Length; i++)
                    args[args.Length - i - 1] = data.Pop();

                if (word is BuiltinWord)
                {
                    foreach (object result in ((BuiltinWord)word).func(args))
                        data.Push(result);
                }
                else
                {
                    Dictionary<string, object> argDict = new Dictionary<string, object>();
                    for (int i = 0; i < args.Length; i++)
                        argDict.Add(word.formals[i], args[i]);
                    call(ident, word.terms, argDict, word.ignoredDynamicWords);
                }
            }
            else if (multimethods.ContainsKey(ident))
                handleMultimethodCall(term, multimethods[ident]);
            else
                throw new UndefinedWordException((IdentTerm)term, calls);
        }
        void handleMultimethodCall(IdentTerm term, MultiMethod multi)
        {
            if (data.Count == 0)
                throw new StackUnderflowException(term, calls);
            object dataTop = data.Pop();
            List<object> objs = (dataTop as List<object>) 
                             ?? new List<object>() { dataTop };
            List<StructObject> _params = new List<StructObject>();
            foreach (object obj in objs)
                if (!(obj is StructObject))
                    throw new InvalidMethodArgException(term.ident, obj, calls);
                else
                    _params.Add((StructObject)obj);
            Method method = multi.getMethod(calls, _params.map(strobj => strobj.getType()).ToArray());

            if (data.Count < method.formals.Length)
                throw new StackUnderflowException((IdentTerm)term, calls);
            object[] args = new object[method.formals.Length];
            for (int i = 0; i < args.Length; i++)
                args[args.Length - i - 1] = data.Pop();

            Dictionary<string, object> argDict = new Dictionary<string, object>();
            for (int i = 0; i < args.Length; i++)
                argDict.Add(method.formals[i], args[i]);
            for (int i = 0; i < method.paramNames.Length; i++)
            {
                if (method.paramNames[i] != null)
                    argDict.Add(method.paramNames[i], _params[i]);
                else data.Push(_params[i]);
            }

            call(term.ident, method.def, argDict, new HashSet<string>());
        }

        [System.Diagnostics.DebuggerNonUserCode]
        CallFrame call(string name, Term[] terms, Dictionary<string, object> args, 
            HashSet<string> ignoredDynamicWords, Quote handler = null, bool canRunCalls = true)
        {
            newCall = true;

            CallFrame frame = new CallFrame(name, terms, ignoredDynamicWords, handler);
            foreach (KeyValuePair<string, object> arg in args)
                frame.setarg(arg.Key, arg.Value);

            calls.call(frame);

            if (calls.depth() > StackOverflowLevel)
                throw new RecursionException(calls);

            if (canRunCalls)
                runCalls();

            return frame;
        }

        [System.Diagnostics.DebuggerNonUserCode]
        public void forceRunCalls()
        {
            alreadyRunning = false;
            runCalls();
        }
        
        bool alreadyRunning = false;
        public void runCalls()
        {
            if (alreadyRunning) return;
            else alreadyRunning = true;

            try
            {
                while (calls.hasFrames())
                {
                    CallFrame top = calls.top();

                    int prevCallDepth = calls.depth();

                    while (top.index < top.terms.Length && !newCall)
                    {
                        try
                        {
                            exec(top.terms[top.index]);
                        }
                        finally
                        {
                            top.index++;
                        }
                    }

                    if (!newCall)
                    {
                        if (calls.depth() >= prevCallDepth)
                            calls.ret();
                        if (top.stopExecution)
                            break;
                    }
                    else newCall = false;
                }
            }
            catch (DataException e)
            {
                while (calls.hasFrames() && calls.top().handler == null)
                    calls.ret();
                if (!calls.hasFrames())
                    throw e;
                CallFrame top = calls.top();
                if (top.handler != null)
                {
                    data.Push(e.data);
                    alreadyRunning = false;
                    exec(top.handler, canRunImmediately: true);
                }
            }
            finally
            {
                alreadyRunning = false;
            }
        }
    }
    class DataException : Exception
    {
        public object data;

        public DataException(object data)
            : base("This DataException was not caught by Concat code.")
        {
            this.data = data;
        }
    }

    [System.Diagnostics.DebuggerNonUserCode]
    class Symbol
    {
        public string sym;

        public Symbol(string sym)
        {
            this.sym = sym;
        }

        public static bool operator ==(Symbol a, Symbol b)
        {
            return a.sym == b.sym;
        }
        public static bool operator !=(Symbol a, Symbol b)
        {
            return a.sym != b.sym;
        }
    }
    [System.Diagnostics.DebuggerNonUserCode]
    class Quote
    {
        public Location loc;
        public string[] formals;
        public Term[] terms;
        public Dictionary<string, object> closure;
        public HashSet<string> ignoredDynamicWords;

        public Quote(Location loc, CallStack calls, string[] formals, params Term[] terms)
        {
            this.loc = loc;
            this.formals = formals;
            this.terms = terms;
            this.ignoredDynamicWords = new HashSet<string>();

            closure = new Dictionary<string, object>();
            HashSet<string> closureVars = terms.map(term => term.closureVars(
                new HashSet<string>(calls.getargs())))
                .combine();
            foreach (string var in closureVars)
                closure.Add(var, calls.getarg(var));
        }

        public void ignoreDynamic(string word)
        {
            ignoredDynamicWords.Add(word);
        }

        public void setFormals(Stack<object> data)
        {
            for (int i = formals.Length - 1; i >= 0; i--)
                closure[formals[i]] = data.Pop();
        }
    }
}
